class_name MainRegion
extends HBoxContainer

signal selected_section_changed
signal sections_reordered
signal window_changed

# Store certain scenes here to avoid cyclic reference errors. When wanting to
# instance the scene use `get_window_type()`
# Use 2 arrays to store the scenes since Dictionary exports don't support
# PackedScene type hints
export(Array, PackedScene) var scenes: Array
export(Array, String) var scene_names: Array
export(PackedScene) var SectionButton_scene: PackedScene = preload("res://src/scenes/sections_list/section_button/section_button.tscn")

# Used to identify which names in `scene_names` are names of sections
var _section_names := PoolStringArray()
# Name of the currently selected section
var _selected_section: String = ""
var _current_window_type: String = ""
var _window_history: Array = []
var _position_in_history: int = 0
var _init_state: Dictionary = {}
onready var _left_column_margin: MarginContainer = \
		find_node("LeftColumnMargin")
onready var _forward_button: Button = find_node("ForwardButton")
onready var _backward_button: Button = find_node("BackwardButton")
onready var _sections_node: ScrollContainer = find_node("Sections")
# Child nodes that shouldn't be removed
onready var _constant_nodes: Array = [_left_column_margin]


func init(sections: Dictionary, sections_order: Array) -> void:
	""" Adds all the sections to the region, in order """
	_section_names.append_array(sections.keys())
	scene_names.append_array(sections.keys())
	scenes.append_array(sections.values())
	# Add all the sections to the scene
	for section_name in _section_names:
		if not (section_name is String):
			var msg: String = ("Key in `sections` is not a String: %s" 
					% str(section_name))
			push_error(msg)
			continue
		add_section(section_name)
	
	# Put the sections in order
	for position in range(len(sections_order)):
		if not (sections_order[position] in _section_names):
			var msg: String = "Value in `sections_order` must also be the name"
			msg += " of a section: %s" % sections_order[position]
			push_warning(msg)
			continue
		set_section_position(sections_order[position], position)
	
	if _init_state.empty():
		sections_order = get_sections_order()
		if len(sections_order) > 0:
			set_window(sections_order[0])
	else:
		_setup_state()


func _ready() -> void:
	_forward_button.connect("pressed", self, "go_forward_in_history")
	_backward_button.connect("pressed", self, "go_backward_in_history")
	_sections_node.connect("item_dropped", self, "_on_Sections_item_dropped")


func add_section(section_name: String) -> void:
	""" Creates the nodes for the section in the sections list """
	if _get_section_nodes(section_name)[0]:
		push_warning("Section already exists: %s" % section_name)
		return
	
	var button: SectionButton = SectionButton_scene.instance()
	button.connect("pressed", self, "_on_SectionButton_pressed", [button])
	button.set_name(section_name)
	button.set_text(section_name)
	_sections_node.add_list_item(button)


func set_section_position(section_name: String, section_position: int) -> bool:
	"""
	Changes the order of the section in the control that shows all the 
	sections. 0 is at the top
	
	Returns:
		`true` if the section's position was successfully updated (may not be 
		exactly to `section_position`), `false` otherwise
	"""
	var button: SectionButton = _get_section_nodes(section_name)[0]
	if button != null:
		_sections_node.move_list_item(button, int(max(section_position, 0)))
		return true
	return false


func get_sections_order() -> PoolStringArray:
	""" Returns the names of the sections in their current order """
	var positions: PoolStringArray = PoolStringArray()
	for section in _sections_node.get_list_items():
		positions.append(section.get_name())
	return positions


func set_window(window_type: String, init_func: String="", init_args: Array=[]
		) -> void:
	"""
	Deselects the selected section if applicable, removes the current window's
	content and saves its state to history, and adds the new window to the
	scene
	"""
	# Will be empty only on first load
	if not _current_window_type.empty():
		# Clear any history that is ahead of the current position in the history
		# because it is no longer valid
		_window_history.resize(_position_in_history)
		# Save the current window's state to the history
		var current_window_state: Dictionary = _get_current_window_state()
		_window_history.append([_current_window_type, current_window_state])
		_position_in_history += 1
		
		_forward_button.set_disabled(true)
		_backward_button.set_disabled(false)
	
	# Replace the current window with the new window
	_set_window(window_type, init_func, init_args)


func add_window_type(window_type: String, scene_path: String) -> void:
	if get_window_type(window_type) == null:
		scene_names.append(window_type)
		scenes.append(load(scene_path))
	else:
		push_error("Window type '%s' already exists" % window_type)


func get_window_type(window_type: String) -> PackedScene:
	"""
	Returns:
		The PackedScene in `scenes` that is at the same index as `window_type`
		is in `scene_names`
	"""
	var idx: int = scene_names.find(window_type)
	if idx > -1 and len(scenes) > idx:
		return scenes[idx]
	return null


func deselect_selected_section() -> void:
	"""
	Deselects the button for the section in the sections list and removes the
	section's content from the scene
	"""
	var old_selection: Array = _get_section_nodes(_selected_section)
	if old_selection[0] != null:
		old_selection[0].set_active(false)
	_selected_section = ""


func remove_current_window() -> void:
	var window: Node = get_child(get_child_count() - 1)
	if not window in _constant_nodes:
		remove_child(window)
		window.queue_free()


func go_forward_in_history(steps: int=1) -> void:
	"""
	Returns to the window that is `steps` ahead of the current in the history
	"""
	assert(steps > 0)
	if _position_in_history >= len(_window_history):
		return
	
	var new_position_in_history := \
			int(min(len(_window_history) - 1, _position_in_history + steps))
	if new_position_in_history != _position_in_history:
		var window_type: String = _window_history[new_position_in_history][0]
		var state: Dictionary = _window_history[new_position_in_history][1]
		
		# Save the current window's state to the history
		var current_window_state: Dictionary = _get_current_window_state()
		_window_history[_position_in_history] = [
			_current_window_type,
			current_window_state
		]
		
		_position_in_history = new_position_in_history
		_forward_button.set_disabled(
				_position_in_history == len(_window_history) - 1)
		_backward_button.set_disabled(_position_in_history == 0)
		
		_set_window(window_type, "set_state", [state])


func go_backward_in_history(steps: int=1) -> void:
	"""
	Returns to the window that is `steps` behind the current in the history
	"""
	assert(steps > 0)
	var new_position_in_history := int(max(0, _position_in_history - steps))
	if (
			(new_position_in_history != _position_in_history)
			and (len(_window_history) > 0)
	):
		var window_type: String = _window_history[new_position_in_history][0]
		var state: Dictionary = _window_history[new_position_in_history][1]
		
		# Save the current window's state to the history
		var current_window_state: Dictionary = _get_current_window_state()
		if _position_in_history == len(_window_history):
			_window_history.append([
				_current_window_type,
				current_window_state
			])
		else:
			_window_history[_position_in_history] = [
				_current_window_type,
				current_window_state
			]
		
		_position_in_history = new_position_in_history
		_forward_button.set_disabled(
				_position_in_history == len(_window_history) - 1)
		_backward_button.set_disabled(_position_in_history == 0)
		
		_set_window(window_type, "set_state", [state])


func get_state() -> Dictionary:
	var state: Dictionary = {
		"selected_section": _selected_section,
		"current_window_type": _current_window_type,
		"current_window_state": _get_current_window_state(),
		"window_history": _window_history,
		"position_in_history": _position_in_history,
	}
	return state


func set_state(state: Dictionary) -> void:
	"""
	Call before the node enters the SceneTree to initialize it with the given
	state
	"""
	_init_state = state


func _setup_state() -> void:
	""" Sets up the scene using the state description in `_init_state` """
	_selected_section = _init_state["selected_section"]
	_current_window_type = _init_state["current_window_type"]
	_window_history = _init_state["window_history"]
	_position_in_history = _init_state["position_in_history"]
	
	_set_window(_current_window_type, "set_state",
			[_init_state["current_window_state"]])
	
	_forward_button.set_disabled(_position_in_history == len(_window_history))
	_backward_button.set_disabled(_position_in_history == 0)


func _get_current_window_state() -> Dictionary:
	var window: Node = get_child(get_child_count() - 1)
	if not window in _constant_nodes and window.has_method("get_state"):
		return window.get_state()
	return {}


func _get_section_nodes(section_name: String) -> Array:
	var section_label: SectionButton = null
	var section_content: Control = get_node_or_null(section_name)
	
	var sections: Array = _sections_node.get_list_items()
	for section in sections:
		if section.get_name() == section_name:
			section_label = section
			break
	
	return [section_label, section_content]


func _set_window(window_type: String, init_func: String="", init_args: Array=[]
		) -> void:
	"""
	Deselects the selected section if applicable, removes the current window's
	content and adds the new window to the scene
	"""
	var new_window: Control = get_window_type(window_type).instance()
	if init_func != "" and new_window.has_method(init_func):
		new_window.callv(init_func, init_args)
	deselect_selected_section()
	remove_current_window()
	add_child(new_window)
	_current_window_type = window_type
	emit_signal("window_changed", new_window)
	
	if window_type in _section_names:
		_set_selected_section(window_type)


func _set_selected_section(section_name: String) -> void:
	""" Highlights the button for the section in the section's list """
	if _selected_section == section_name:
		return
	
	var section_label: Button = _get_section_nodes(section_name)[0]
	if section_label != null:
		section_label.set_active(true)
		_selected_section = section_name
		emit_signal("selected_section_changed", section_name)


func _on_SectionButton_pressed(button: SectionButton) -> void:
	var section_name: String = button.get_name()
	set_window(section_name)


func _on_Sections_item_dropped() -> void:
	emit_signal("sections_reordered", self)
